/*
 * Created on Feb 12, 2008
 */
package maxygen;

import java.util.HashSet;

import com.cycling74.max.*;

/**
 * A wrapper class for MaxBox.
 * 
 * @see     com.cycling74.max.MaxBox
 * @author  martinrobinson
 */
class BoxWrapper {
    
    /** The wrapped box */
    private MaxBox b;
    
    /**
     * The varieties of sorting available on BoxWrappers.
     */
    enum Sort { 
        /** Don't sort boxes */
        NONE, 
        
        /** Sort boxes by name tag */
        TAG, 
        
        /** Sort boxes by patcher position */
        POS 
    }
    
    enum TagMode { MULTI, SINGLE }
    
    /** Dimensions array index for the left rectangle edge */
    private static final int RECT_LEFT = 0;
    
    /** Dimensions array index for the top rectangle edge */
    private static final int RECT_TOP = 1;
    
    /** Dimensions array index for the right rectangle edge */
    private static final int RECT_RIGHT = 2;
    
    /** Dimensions array index for the botom rectangle edge */
    private static final int RECT_BOTTOM = 3;
    
    /** Used as an argument when requesting multiply indexed tags*/
    //public static final boolean TAGS_MULTI = true;
    
    /** Used as an argument when requesting single tags (i.e., no index)*/
    //public static final boolean TAGS_SINGLE = false;
    
    /**
     * Wraps a <code>MaxBox</code> in a <code>BoxWrapper</code>
     * @param b the <code>MaxBox</code> to wrap
     */
    BoxWrapper(MaxBox b)
    {
        this.b = b;
    }
    
    /**
     * Get the box's <code>MaxPatcher</code> as a <code>PatcherWrapper</code>
     * @return the box's patcher
     */
    PatcherWrapper getSubPatcher()
    {
        return new PatcherWrapper(b.getSubPatcher());
    }
    
    /**
     * Query whether the box contains a patcher (i.e., it is a subpatcher)
     * @param mode      which type of subpatcher to include in the search
     * @return          whether the box is of the type queried
     * @see             PatcherWrapper.SubType
     */
    boolean isBoxPatcher(PatcherWrapper.SubType mode)
    {   
        String maxclass = b.getMaxClass();
        String name = b.getName();
        
        if(name != null) {
            ObjectName on = new ObjectName(name);
            name = on.getName();
        }
        
        switch(mode) {
        case MORE:
            return maxclass.equals("patcher") && name != null && name.equals("more");
        case NOTMORE:
            return maxclass.equals("patcher") && (name == null || !name.equals("more"));
        case ALL:
        default:        
            return maxclass.equals("patcher");
        }
           
    }
    
    /**
     * send an arbitrary message to the patcher. See the help file for the 
     * max external thispatcher to see the sorts of messages you might want to send.
     * @param msg   the message to send thispatcher
     * @param list  arguments for the message to thispatcher. can be null.
     */
    void send(String msg, Atom list[])
    {
        b.send(msg, list);
    }
    
    /**
     * In a patcher box names can be set via the Max application Object menu name... item. 
     * By naming a box you can have access to it via the getNamedBox method of 
     * <code>MaxPatcher</code>
     * @return      the scripting name of the box
     */
    String getName()
    {
        return b.getName();
    }
    
    /**
     * Set the name of this box. This is the same as seeting the name of the 
     * box via the "name..." menu item in the Object menu of the Max application.
     * @param name  the scripting name of the box
     */
    void setName(String name)
    {
        b.setName(name);
    }
    
    /**
     * Gets the patcher relative screen coordinates of the box. x1,y1 is 
     * the upper left hand corner of the box and x2,y2 is the lower right hand corner.
     * @return      int array with four elements in the following order: x1,y1,x2,y2
     */
    int[] getRect()
    {
        return b.getRect();
    }
    
    String getMaxClass()
    {
        return b.getMaxClass();
    }
    
    MaxPatcher getPatcher()
    {
        return b.getPatcher();
    }
    
    MaxBox unWrap()
    {
        return b;
    }
    
    /**
     * Determines if the box is tagged with a particular name or one of its indices.
     * @param tag       the tag name (without an index)
     * @param mode      whether to include indices [1], [2] etc in the test
     * @return          whether the box is named with the tag
     */
    boolean isBoxTagged(String tag, TagMode mode)
    {   
        /*
         * could rewrite this using the new ObjectName class
         * ** perhaps it's better like this with the **
         * ** new region detail functionality **
         */
        String name = b.getName();
        
        if(name == null)
            return false;   
        else if(name.equals(tag) || (mode==TagMode.MULTI && name.startsWith(tag+"[")) )
            return true;
        else
            return false;
    }
    
    /**
     * Determines if the box is tagged with a particular name.
     * @param tag       the tag name (without an index)
     * @return          whether the box is named with the tag
     */
    boolean isBoxTaggedSingle(String tag)
    {   
        return isBoxTagged(tag, TagMode.SINGLE);
    }
    
    /**
     * Determines if the box is tagged with a particular name or one of its indices.
     * @param tag       the tag name (without an index)
     * @return          whether the box is named with the tag
     */
    boolean isBoxTaggedMulti(String tag)
    {   
        return isBoxTagged(tag, TagMode.MULTI);
    }
    
    /**
     * Wrap a <code>MaxBox</code> array
     * @param boxes     the boxes to be wrapped
     * @return          the wrapped boxes
     */
    static BoxWrapper[] wrapBoxes(MaxBox boxes[])
    {
        BoxWrapper wrappedBoxes[] = new BoxWrapper[boxes.length];
        for(int i = 0; i < boxes.length; i++) {
            wrappedBoxes[i] = new BoxWrapper(boxes[i]);
        }
        
        return wrappedBoxes;
    }
    
    /**
     * Find all boxes tagged with a particular name or one of its indices.
     * @param boxes     the array of boxes to search
     * @param tag       the tag to search for
     * @return          the boxes with the selected tag
     */
    static BoxWrapper[] findTaggedBoxes(BoxWrapper boxes[], String tag)
    {
        int count = 0;
        int i;
        
        for(i = 0; i < boxes.length; i++) {
            if(boxes[i].isBoxTaggedMulti(tag))
                count++;
        }
        
        BoxWrapper taggedBoxes[] = new BoxWrapper[count];
        
        int j = 0;
        for(i = 0; i < boxes.length; i++) {
            if(boxes[i].isBoxTaggedMulti(tag))
                taggedBoxes[j++] = boxes[i];
        }
        
        return taggedBoxes;
    }
    
    /**
     * Find one boxes tagged with a particular name.
     * @param boxes     the array of boxes to search
     * @param tag       the tag to search for
     * @return          the box with the selected tag
     */
    static BoxWrapper findTaggedBox(BoxWrapper boxes[], String tag)
    {
        for(int i = 0; i < boxes.length; i++) {
            if(boxes[i].isBoxTaggedSingle(tag))
                return boxes[i];
        }
        
        return null;
    }
    
    /**
     * Determine if two boxes are already sorted in the correct order according to tag index.
     * @param low       the box which should be in the low position
     * @param high      the box which should be in the right position
     * @return          whether the boxes are already sorted or in equal position
     */
    private static boolean areSortedTag(BoxWrapper low, BoxWrapper high)
    {
        ObjectName lowName = new ObjectName(low.getName());
        ObjectName highName = new ObjectName(high.getName());
                
        if(!lowName.getName().equals(highName.getName())) {
            MaxObject.post("BoxWarpper.areSortedTag(), names should differ only by index");
            return true;
        } else {
            
            if(lowName.getIndex() <= highName.getIndex())
                return true;
            else
                return false;
       
        }
    }
    
    /**
     * Determine if two boxes are already sorted in the correct order.
     * @param low       the box which should be in the low position
     * @param high      the box which should be in the higg position
     * @param sortMode  which type of sort is being performed
     * @return          whether the boxes are already sorted or in equal position according
     *                  to the sort type selected
     */
    private static boolean areSorted(BoxWrapper low, BoxWrapper high, Sort sortMode)
    {
        switch(sortMode) {
        case POS:
            return areSortedPos(low,high);
        case TAG:
            return areSortedTag(low,high);
        default:
            return true;    
        }
    }
    
    /**
     * Determine if two boxes are already sorted in the correct order according
     * to screen/patcher position.
     * @param low       the box which should be in the low position
     * @param high      the box which should be in the right position
     * @return          whether the boxes are already sorted or in equal position
     */
    private static boolean areSortedPos(BoxWrapper low, BoxWrapper high)
    {
        int lowRect[] = low.getRect();
        int highRect[] = high.getRect();
        
        if(lowRect[RECT_LEFT] == highRect[RECT_LEFT]) {  
            /*
             *  if two inlets are aligned vertically the inlet lower on the screen
             *  will be to the left so in this case we consider > to be "lower"
             */
            if(lowRect[RECT_TOP] >= highRect[RECT_TOP])
                return true;
            else
                return false;
            
        } else if(lowRect[RECT_LEFT] <= highRect[RECT_LEFT]) {
            return true;
        } else {
            return false;
        }
    }
    
    /**
     * Swap two boxes in an array
     * @param boxes     the box array
     * @param i         index of one of the boxes to swap
     * @param j         index of the other box to swap
     */
    private static void swapBoxes(BoxWrapper boxes[], int i, int j)
    {
        BoxWrapper tmp = boxes[i];
        boxes[i] = boxes[j];
        boxes[j] = tmp;
    }
    
    /**
     * Sort boxes according to tag name or screen/patcher position
     * @param boxes         the array of boxes to sort
     * @param sortType      which type of sort is being performed
     * @return              the sorted array of boxes
     */
    static BoxWrapper[] sortBoxes(BoxWrapper boxes[], Sort sortType)
    {
        /*
         * bubble sort shouldn't be too inefficient 
         * for the likely number of boxes in question
         */
        if(boxes.length < 2) return boxes;
        
        int numCompares = boxes.length-1;
        boolean swapped;
        
        do {
            swapped = false;
            for(int i = 0; i < numCompares; i++) {
                if(!areSorted(boxes[i], boxes[i+1], sortType )) {
                    swapBoxes(boxes, i, i+1);
                    swapped = true;
                }
            }
        } while(swapped);
        
        return boxes;
    }
    
    /**
     * Sort boxes according to screen/patcher position
     * @param boxes     the array of boxes to sort
     * @return          the sorted array of boxes
     */
    static BoxWrapper[] sortBoxesPos(BoxWrapper boxes[])
    {
        return sortBoxes(boxes, Sort.POS);
    }
    
    /**
     * Sort boxes according to tag name index position
     * @param boxes     the array of boxes to sort
     * @return          the sorted array of boxes
     */
    static BoxWrapper[] sortBoxesTag(BoxWrapper boxes[])
    {
        return sortBoxes(boxes, Sort.TAG);
    }
    
    /**
     * Renumber boxes tag indices. Indices are renumber at increments specified (e.g., 10)
     * to enable ease of reordering by hand later. Increment is forced to be 1 if it is less 
     * than 1.
     * @param boxes         the array of boxes to renumber
     * @param increment     the increment for the renumber counter
     * @return              the renumbered array of boxes
     */
    static BoxWrapper[] renumberBoxTags(BoxWrapper boxes[], int increment)
    {
        if(increment < 1) increment = 1;
        
        ObjectName ons[] = new ObjectName[boxes.length];
        
        int i, j = 0;
        for(i = 0; i < boxes.length; i++) {
            String name = boxes[i].getName();
            if(name != null) {
                ons[i] = new ObjectName(name);
                ons[i].setIndex(j);
                j+=increment;
                
                //hashed name null crashes on save?
                boxes[i].setName(StringUtility.generateNameString(boxes[i]));
            }
            else {
                ons[i] = null;
            }
        }
                
        // renumber
        for(i = 0; i < boxes.length; i++) {
            if(ons[i] != null) 
                boxes[i].setName(ons[i].getFullName());
        }
                
        return boxes;
    }
    
    /**
     * Determine if a box tag name already exists to avoid conflicts
     * @param boxes     the array of boxes to test
     * @return          whether the boxes are all uniquely named
     */
    static boolean boxTagsAreUnique(BoxWrapper boxes[])
    {        
        HashSet<String> set = new HashSet<String>();
        
        for(int i = 0; i < boxes.length; i++) {
            String name = boxes[i].getName();
            if(set.contains(name))
                return false;
            else
                set.add(name);
        }
        
        return true;
    }
}
